#include "uc1701x.h"
#include "font.h"

#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_gpio.h"
#include "stm32f0xx_ll_spi.h"
#include "stm32f0xx_ll_utils.h"

#define ENABLE_SOFTWARE_NSS     1


#define LCD_SPI                 SPI1
#define LCD_SCK_Port            GPIOA
#define LCD_SCK_Pin             LL_GPIO_PIN_5
#define LCD_SDA_Port            GPIOA
#define LCD_SDA_Pin             LL_GPIO_PIN_7

#define LCD_CS_PORT             GPIOA
#define LCD_CS_Pin              LL_GPIO_PIN_4
#define LCD_SET_CS_HIGH()       LL_GPIO_SetOutputPin(LCD_CS_PORT, LCD_CS_Pin)
#define LCD_SET_CS_LOW()        LL_GPIO_ResetOutputPin(LCD_CS_PORT, LCD_CS_Pin)

#define LCD_RST_Port            GPIOB
#define LCD_RST_Pin             LL_GPIO_PIN_0
#define LCD_SET_RST_HIGH()      LL_GPIO_SetOutputPin(LCD_RST_Port, LCD_RST_Pin)
#define LCD_SET_RST_LOW()       LL_GPIO_ResetOutputPin(LCD_RST_Port, LCD_RST_Pin)

#define LCD_RS_Port             GPIOA
#define LCD_RS_Pin              LL_GPIO_PIN_6
#define LCD_SET_RS_HIGH()       LL_GPIO_SetOutputPin(LCD_RS_Port, LCD_RS_Pin)
#define LCD_SET_RS_LOW()        LL_GPIO_ResetOutputPin(LCD_RS_Port, LCD_RS_Pin)


lcd_style_t lcd_style;

/**
 * @brief   Transmit command to uc1701x.
 * @param   command: One byte command to set LCD.
 * @retval  None
 */
static void lcd_transmit_command(uint8_t command)
{
    LCD_SET_RS_LOW();

    while(!LL_SPI_IsActiveFlag_TXE(LCD_SPI));           /* Waiting for SPI TX empty */
    LL_SPI_TransmitData8(LCD_SPI, command);
    while(!LL_SPI_IsActiveFlag_TXE(LCD_SPI));           /* Waiting for SPI TX empty */
}

/**
 * @brief   Transmit data to uc1701x.
 * @param   data: One byte data to display.
 * @retval  None
 */
static void lcd_transmit_data(uint8_t data)
{
    LCD_SET_RS_HIGH();

    while(!LL_SPI_IsActiveFlag_TXE(LCD_SPI));           /* Waiting for SPI TX empty */
    LL_SPI_TransmitData8(LCD_SPI, data);
    while(!LL_SPI_IsActiveFlag_TXE(LCD_SPI));           /* Waiting for SPI TX empty */
}

/**
 * @brief   Set the SRAM column address before Write/Read memory.
 * @param   page: Page Address of DDRAM. Value range 0-7.
 * @param   column: Column Address of DDRAM. Value range 0-127.
 * @retval  None
 * @note    The page address of uc1701x's DDRAM is up to 8 and column address is up to 131.
            But almost LCD only features 64 rows (8 pages) and 128 column.
 */
static void lcd_set_pos(uint8_t page, uint8_t column)
{
#if LCD_INVERSION
  column += 4;
#endif

    lcd_transmit_command(0xb0 | page);
    lcd_transmit_command(column >> 4 & 0x0f | 0x10);
    lcd_transmit_command(column & 0x0f);
}

void spi_init(void)
{
    LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
    LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOB);
    LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_SPI1);

    /* Initial RESET pin */
    LL_GPIO_ResetOutputPin(LCD_RST_Port, LCD_RST_Pin);
    LL_GPIO_SetPinMode(LCD_RST_Port, LCD_RST_Pin, LL_GPIO_MODE_OUTPUT);
    LL_GPIO_SetPinOutputType(LCD_RST_Port, LCD_RST_Pin, LL_GPIO_OUTPUT_PUSHPULL);
    LL_GPIO_SetPinPull(LCD_RST_Port, LCD_RST_Pin, LL_GPIO_PULL_NO);
    LL_GPIO_SetPinSpeed(LCD_RST_Port, LCD_RST_Pin, LL_GPIO_SPEED_FREQ_LOW);

    /* Initial RS pin */
    LL_GPIO_ResetOutputPin(LCD_RS_Port, LCD_RS_Pin);
    LL_GPIO_SetPinMode(LCD_RS_Port, LCD_RS_Pin, LL_GPIO_MODE_OUTPUT);
    LL_GPIO_SetPinOutputType(LCD_RS_Port, LCD_RS_Pin, LL_GPIO_OUTPUT_PUSHPULL);
    LL_GPIO_SetPinPull(LCD_RS_Port, LCD_RS_Pin, LL_GPIO_PULL_NO);
    LL_GPIO_SetPinSpeed(LCD_RS_Port, LCD_RS_Pin, LL_GPIO_SPEED_FREQ_LOW);

#if ENABLE_SOFTWARE_NSS == 1
    /* Initial NSS pin */
    LL_GPIO_ResetOutputPin(LCD_CS_PORT, LCD_CS_Pin);
    LL_GPIO_SetPinMode(LCD_CS_PORT, LCD_CS_Pin, LL_GPIO_MODE_OUTPUT);
    LL_GPIO_SetPinOutputType(LCD_CS_PORT, LCD_CS_Pin, LL_GPIO_OUTPUT_PUSHPULL);
    LL_GPIO_SetPinPull(LCD_CS_PORT, LCD_CS_Pin, LL_GPIO_PULL_NO);
    LL_GPIO_SetPinSpeed(LCD_CS_PORT, LCD_CS_Pin, LL_GPIO_SPEED_FREQ_LOW);
#else
    LL_GPIO_SetPinMode(LCD_CS_PORT, LCD_CS_Pin, LL_GPIO_MODE_ALTERNATE);
    LL_GPIO_SetAFPin_0_7(LCD_SCK_Port, LCD_SCK_Pin, LL_GPIO_AF_0);
    LL_GPIO_SetPinOutputType(LCD_CS_PORT, LCD_CS_Pin, LL_GPIO_OUTPUT_PUSHPULL);
    LL_GPIO_SetPinPull(LCD_CS_PORT, LCD_CS_Pin, LL_GPIO_PULL_NO);
    LL_GPIO_SetPinSpeed(LCD_CS_PORT, LCD_CS_Pin, LL_GPIO_SPEED_FREQ_HIGH);
#endif

    /* Initial SCK pin */
    LL_GPIO_SetPinMode(LCD_SCK_Port, LCD_SCK_Pin, LL_GPIO_MODE_ALTERNATE);
    LL_GPIO_SetAFPin_0_7(LCD_SCK_Port, LCD_SCK_Pin, LL_GPIO_AF_0);
    LL_GPIO_SetPinOutputType(LCD_SCK_Port, LCD_SCK_Pin, LL_GPIO_OUTPUT_PUSHPULL);
    LL_GPIO_SetPinPull(LCD_SCK_Port, LCD_SCK_Pin, LL_GPIO_PULL_NO);
    LL_GPIO_SetPinSpeed(LCD_SCK_Port, LCD_SCK_Pin, LL_GPIO_SPEED_FREQ_HIGH);

    /* Initial SDA pin */
    LL_GPIO_SetPinMode(LCD_SDA_Port, LCD_SDA_Pin, LL_GPIO_MODE_ALTERNATE);
    LL_GPIO_SetAFPin_0_7(LCD_SDA_Port, LCD_SDA_Pin, LL_GPIO_AF_0);
    LL_GPIO_SetPinOutputType(LCD_SDA_Port, LCD_SDA_Pin, LL_GPIO_OUTPUT_PUSHPULL);
    LL_GPIO_SetPinPull(LCD_SDA_Port, LCD_SDA_Pin, LL_GPIO_PULL_NO);
    LL_GPIO_SetPinSpeed(LCD_SDA_Port, LCD_SDA_Pin, LL_GPIO_SPEED_FREQ_HIGH);

    /* Initial SPI */
    LL_SPI_SetMode(LCD_SPI, LL_SPI_MODE_MASTER);
    LL_SPI_SetClockPolarity(LCD_SPI, LL_SPI_POLARITY_HIGH);
    LL_SPI_SetClockPhase(LCD_SPI, LL_SPI_PHASE_2EDGE);
    LL_SPI_SetBaudRatePrescaler(LCD_SPI, LL_SPI_BAUDRATEPRESCALER_DIV8);
    LL_SPI_SetTransferBitOrder(LCD_SPI, LL_SPI_MSB_FIRST);
    LL_SPI_SetTransferDirection(LCD_SPI, LL_SPI_HALF_DUPLEX_TX);
#if ENABLE_SOFTWARE_NSS == 1
    LL_SPI_SetNSSMode(LCD_SPI, LL_SPI_NSS_SOFT);
#else
    LL_SPI_SetNSSMode(LCD_SPI, LL_SPI_NSS_HARD_OUTPUT);
#endif
    LL_SPI_DisableCRC(LCD_SPI);
    LL_SPI_SetDataWidth(LCD_SPI, LL_SPI_DATAWIDTH_8BIT);

    LL_SPI_Enable(LCD_SPI);
}

/**
 * @brief   Initializes the LCD.
 * @retval  None
 */
void lcd_init(void)
{
    LCD_SET_RST_LOW();
    LL_mDelay(2);
    LCD_SET_RST_HIGH();
    LL_mDelay(10);

    lcd_transmit_command(0xe2);      /* System reset */
    lcd_transmit_command(0xae);      /* Set display disable */
    lcd_transmit_command(0x40);      /* Set scroll line 0 */
    lcd_transmit_command(0xa6);      /* Set Foreground color black and Background color white */
    lcd_transmit_command(0xa4);      /* Normal display, don't turn on all pixels */

#if LCD_INVERSION
    lcd_transmit_command(0xa1);      /* Column Scanning sequence, from right to left (reverse direction) */
    lcd_transmit_command(0xc0);      /* Row scanning sequence, from bottom to top (normal direction) */
#else
    lcd_transmit_command(0xa0);      /* Column Scanning sequence, from left to right (normal direction) */
    lcd_transmit_command(0xc8);      /* Row scanning sequence, from top to bottom (reverse direction) */
#endif

    lcd_transmit_command(0x2f);      /* Enable the built-in power circuits */
    lcd_transmit_command(0x24);      /* Roughly adjust the contrast */

    LL_mDelay(150);
    lcd_transmit_command(0xa2);      /* Set bias 1/9 */

    lcd_transmit_command(0x81);      /* Finely adjust the contrast */
    lcd_transmit_command(0x1f);      /* The value to adjust the contrast finely */

    lcd_transmit_command(0xaf);      /* set dispaly enable */
}

/**
 * @brief   Clear designated area. The area includes start address but not end address.
 * @param   start_page: start page address.
 * @param   start_column: start column address.
 * @param   end_page: end page address.
 * @param   end_column: end column address.
 * @retval  None
 * @note    Clear rang is from start_page, start_column to end_page-1 , end_column-1.
 */
void lcd_clear(uint8_t start_page, uint8_t start_column, uint8_t end_page, uint8_t end_column)
{
    if(start_page >= end_page || start_column >= end_column || end_page > LCD_PAGE_SUM || end_column > LCD_COLUMN_SUM)
    {
      return;
    }

    uint8_t i, j;

    for(i = start_page; i < end_page; i++)
    {
      lcd_set_pos(i, start_column);
      for(j = start_column; j < end_column; j++)
      {
        lcd_transmit_data(0x00);
      }
    }
}

/**
 * @brief   Clear whole LCD screen.
 * @retval  None
 */
void lcd_clear_all(void)
{
  lcd_clear(0, 0, LCD_PAGE_SUM, LCD_COLUMN_SUM);
}

/**
 * @brief   Display one character.
 * @param   page: Page address where to display the character shape.
            This parameter can be one of the following values:
              @arg  0,2,4,6:          if the font is FONT8_16
              @arg  0,1,2,3,4,5,6,7:  if the font is FONT6_8
 * @param   column: Column address where to display the character shape.
 * @param   character: Character to display.
 * @param   style: Style of character shape.
 * @retval  None
 */
void lcd_display_char(uint8_t page, uint8_t column, uint8_t character, lcd_style_t *style)
{
    uint8_t c, i;

    c = character - ' ';

    if(style->font == LCD_FONT8_16)
    {
        lcd_set_pos(page, column);

        for(i = 0; i < 8; i++)
        {
            if(style->reverse == LCD_TEXT_REVERSE)
            {
                lcd_transmit_data(~FONT8x16[c * 16 + i]);
            }
            else
            {
              lcd_transmit_data(FONT8x16[c * 16 + i]);
            }
        }

        lcd_set_pos(page + 1, column);

        for(i = 0; i < 8; i++)
        {
            if(style->reverse == LCD_TEXT_REVERSE)
            {
                lcd_transmit_data(~FONT8x16[c * 16 + i + 8]);
            }
            else
            {
                lcd_transmit_data(FONT8x16[c * 16 + i + 8]);
            }
        }
    }
    else if(style->font == LCD_FONT6_8)
    {
        lcd_set_pos(page, column);

        for(i = 0; i < 6; i++)
        {
            if(style->reverse == LCD_TEXT_REVERSE)
            {
                lcd_transmit_data(~FONT6x8[c][i]);
            }
            else
            {
                lcd_transmit_data(FONT6x8[c][i]);
            }
        }
    }
    else
    {
        /* undefine font */
    }
}

/**
 * @brief   Display string.
 * @param   page: Page address where to display the string.
            This parameter can be one of the following values:
              @arg  0,2,4,6:          if the font is FONT8_16.
              @arg  0,1,2,3,4,5,6,7:  if the font is FONT6_8.
 * @param   column: Column address where to display the string.
 * @param   character: Pointer to string to display on LCD.
 * @param   p_style: Style of string.
 * @retval  None
 */
void lcd_display_string(uint8_t page, uint8_t column, uint8_t *character, lcd_style_t *p_style)
{
    uint8_t i = 0;

    while(character[i] != '\0')
    {
        lcd_display_char(page, column, character[i], p_style);

        if(p_style->font == LCD_FONT8_16)
        {
            column += 8;

            if(column > LCD_COLUMN_SUM - 8)
            {
                column = 0;
                page += 2;
            }
        }
        else if(p_style->font == LCD_FONT6_8)
        {
            column += 6;

            if(column > LCD_COLUMN_SUM - 6)
            {
                column = 0;
                page += 2;
            }
        }
        else
        {
            /* undefine font */
        }

        i++;
    }
}

/**
 * @brief   Display image.
 * @param   x0: column position where to start display image. Range 0-64.
 * @param   y0: row position where to start display image.Range 0-128.
 * @param   x1: column position where to end display image.Range 0-64.
 * @param   y1: row position where to end display image.Range 0-128.
 * @retval  None
 * @note    Dispaly image rang is from x0, y0 to x1-1 ,y1-1.
 *          Start row will be aligned to next page if it isn't multiples of 8.
 */
void lcd_display_bmp(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t const *p_bmp)
{
    uint32_t i = 0;

    uint8_t x, y;

    y0 = (y0 & 0x07) ? ((y0 >> 0x03) + 1) : (y0 >> 0x03);
    y1 = (y1 & 0x07) ? ((y1 >> 0x03) + 1) : (y1 >> 0x03);

    for (y = y0; y < y1; y++)
    {
        lcd_set_pos(y, x0);

        for (x = x0; x < x1; x++)
        {
            lcd_transmit_data(p_bmp[i++]);
        }
    }
}
